/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package chatserver;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Scanner;
import shared.HeaderConstants;

/**
 *
 * @author freeman
 */
public class Server extends Thread {

    private ServerSocket server = null;
    private final Object serverFull = new Object();
    private HashMap<Integer, ServerThread> clientThreads = null;
    private HashMap<Integer, String> usernameMap = null;
    private int curClientId = 0;
    private int portNo = 0;
    private final int SVR_MAX_CLIENTS = 20;
    private boolean isAlive = true;
    private Date stTime = null;

    /*
     * Constructor to create a server to listen on a part and 
     * listen to clients.
     * 
     * @param porNo: The number port to listen on.
     */
    public Server( int portNo ) {
        this.portNo = portNo;
        clientThreads = new HashMap<Integer, ServerThread>();
        usernameMap = new HashMap<Integer, String>();
    }

    @Override
    public void run() {
        /*save off the datetime that the server started hosting*/
        DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
        Date date = new Date();
        stTime = date;
        
        System.err.println( "Server: Running, started at " +  dateFormat.format(date));
        //establish the server.
        try {
            server = new ServerSocket( this.portNo );
        } catch ( IOException ex ) {
            System.err.println( "Server: Cannot start server: " + ex.getMessage() );
            return;
        }

        //try and listen for clients in a loop
        try {
            listenAndAccept();
        } catch ( InterruptedException ie ) {
            System.err.println( "Server: Interrupted! Quitting..." );
        }
        System.err.println( "Server: Stopping..." );
        try {
            server.close();
        } catch ( IOException ex ) {
            System.err.println( "Server: Could not close server socket." );
        }
    }

    /*
     * Listens and accepts clients in a loop. Throws exceptions if
     * this fails.
     */
    private void listenAndAccept() throws InterruptedException {
        while ( isRunning() ) {
            /*atomic reads from curclientid*/
            synchronized ( serverFull ) {
                if ( curClientId == SVR_MAX_CLIENTS ) {

                    while ( curClientId == SVR_MAX_CLIENTS ) {
                        System.out.println( "Max clients (" + SVR_MAX_CLIENTS
                                + ") reached..." );
                        serverFull.wait();
                    }
                }
            } //end sync

            /*accept a new client*/
            Socket client = null;
            try {
                System.err.println( "Server: Blocking on accept client..." );
                client = server.accept();
                System.err.println( "Server: server.accept() completed..." );
            } catch ( IOException ex ) {
                if ( isAlive() ) {
                    System.err.println( "Server: Server is terminated, cannot read from socket." );
                } else {
                    System.err.println( "Server: I/O Exception: "
                            + ex.getMessage() + " when trying to accept client." );
                }
            }

            /*if we accepted a good client, store and start them*/
            if ( client != null ) {
                /*atomic reads and writes to curClientId and clientThreads*/
                synchronized ( serverFull ) {
                    System.out.println( "Server accepted client " + curClientId );

                    ServerThread serverThread = new ServerThread( this, client,
                            curClientId );
                    clientThreads.put( curClientId++, serverThread );
                    serverThread.start();
                } //end sync

            }

        }
        System.err.println( "Server is exiting..." );
    }

    /*
     * Terminates the Server
     */
    private synchronized void terminate() {
        System.out.println( "Server: Terminated..." );
        isAlive = false;
        
        /*kick all users*/
        //broadcastCommand( HeaderConstants.HEADER_KICKED, "Server is closing".getBytes(), -1);
        
        /*close the server*/
        try {
            server.close();
        } catch ( IOException ex ) {
            System.err.println( "Server: Could not close server socket..." );
        }
    }

    /*
     * Checks to see if the server is alive, or terminated.
     * 
     * @return true if the server is alive, false otherwise.
     */
    private synchronized boolean isRunning() {
        return isAlive;
    }

    /*
     * Send a server message to each client.
     * 
     * @param bytes: The message to send
     * @param id: specify which ID to send bytes to (-1 for all)
     */
    private synchronized void broadcastMessage( byte[] bytes, int id ) {
        String message = "***Server***> ";
        message = message + new String( bytes );

        outputToClients( HeaderConstants.HEADER_SERVERMSG, message.getBytes(), id );
    }

    /*
     * Send a server command to each client.
     * 
     * @param header: The header for the message
     * @param bytes: The message to send
     * @param id: specify which ID to send bytes to (< 0 for all)
     */
    private synchronized void broadcastCommand( byte[] header, byte[] bytes, int id ) {
        outputToClients( header, bytes, id );
    }

    /*
     * Registers a username sent from the client to the id associated 
     * with its thread controlling its socket.
     * 
     * @param username: The username of the client
     * @param id: The id of the thread controlling its socket
     */
    public synchronized void serverRegisterUsername( String username, int id ) {
        int userNameSuffix = 0;
        String userNameFinal = username;
        boolean validating = true;

        /*make sure the name is unique, change it if it is not*/
        while ( validating ) {
            validating = false;
            for ( Integer k : usernameMap.keySet() ) {
                if ( usernameMap.get( k ).equals( username ) ) {
                    userNameSuffix++;
                    userNameFinal = username + userNameSuffix;
                    validating = true;
                    break;
                }
            }

        }

        /*store the final username*/
        username = userNameFinal;

        /*if we had to change the clients username, let them know*/
        if ( userNameSuffix > 0 ) {
            broadcastCommand( HeaderConstants.HEADER_CHANGEUSERNAME,
                    username.getBytes(), id );
        }

        /*add the username to the username-id map*/
        usernameMap.put( id, username );

        /*tell everyone that user joined the server*/
        broadcastMessage( ( username + " joined the server." ).getBytes(), -1 );

        /*send everyone an updated user list*/
        broadcastCommand( HeaderConstants.HEADER_SVRUSRLIST,
                getUsernameList().getBytes(), -1 );

    }

    /*
     * ServerThread calls this when a message is received from client 'id', 
     * where the contents are in 'string'.
     * 
     * @param string: the contents of the message
     * @param id: the client's identifier.
     */
    public synchronized void serverMessagePubChat( byte[] bytes, int id ) {
        String fromUser = usernameMap.get( id );
        String message = fromUser + "> " + new String( bytes );
        message = message.trim();
        //rework this to be more direct...
        outputToClients( HeaderConstants.HEADER_CHATMSG_PUB, message.getBytes(), -1 );

    }


    /*
     * ServerThread calls this method when the client either quits, or something
     * goes wrong and the client is lost.
     * 
     * @param id: The id of the read that called this removeMe function
     * @param reason: the reason this thread needs to be removed
     */
    public synchronized void serverRemoveMe( int id ) {
        String usernameThatLeft;
        /*atmomic reads and writes to curClientId and clientThreads*/
        synchronized ( serverFull ) {
            if ( clientThreads.get( id ) == null ) {
                System.err.println( "Server: Bad remove id: " + id );
                return;
            }
            System.err.println( "Server: Client " + id + " left." );
            clientThreads.remove( id );
            usernameThatLeft = usernameMap.remove( id );
            curClientId--;
            serverFull.notify();
        }

        /*prepare a message to send*/
        String msg = "Client [" + usernameThatLeft + "] left";

        /*send the message to all the clients telling them who left*/
        broadcastMessage( msg.getBytes(), -1 );

        /*send everyone an updated user list*/
        broadcastCommand( HeaderConstants.HEADER_SVRUSRLIST,
                getUsernameList().getBytes(), -1 );
    }

    /*
     * returns a string delimitted with a colon of usernames in the 
     * list of usernames on the server.
     * 
     * @return a string, ':' delimited, containing the usernames.
     * */
    public synchronized String getUsernameList() {
        if ( usernameMap.isEmpty() ) {
            return "";
        }

        String usernameList = ":";

        for ( Integer k : usernameMap.keySet() ) {
            usernameList += usernameMap.get( k ) + ":";
        }

        return usernameList.substring( 0, usernameList.length() - 1 );
    }

    /*
     * Sends data to one client or all
     * 
     * @param header: The header to send
     * @param bytes: the message to send
     * @param id: if < 0, all clients recieve the data. Otherwise, only
     *  client id receives the data.
     */
    private synchronized void outputToClients( byte[] header, byte[] bytes, int id ) {
        if ( id < 0 ) {
            for ( Integer k : clientThreads.keySet() ) {
                clientThreads.get( k ).sendData( header, bytes );
            }
            return;
        }
        clientThreads.get( id ).sendData( header, bytes );
    }
    
    /*
     * Prints the status of the server to std out.
     */
    private synchronized void printStatus() {
        /*compute uptime*/
        DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
        DateFormat dateFormatUpTime = new SimpleDateFormat("HH:mm:ss");
        Date date = new Date();
        
        long upTime = date.getTime() - stTime.getTime();
        
        Date upTimeDate = new Date( upTime );

        System.out.println("========BEGIN SERVER STATUS=======");
        System.out.println("Current local system time: " +
                dateFormat.format(date) + ", uptime: " +
                dateFormatUpTime.format(upTimeDate));
        System.out.println("Server hosted on port " + portNo );
        System.out.println(usernameMap.size() + " users connected:");
        
        /*print out each user*/
        for ( String k : usernameMap.values() ) {
            System.out.println("User: " + k );
        }
        
        System.out.println("========END SERVER STATUS=======");
    }

    /**
     * @param args arg[0] is port number
     */
    public static void main( String[] args ) {
        int portNo = 45537;
        if ( args.length != 1 ) {
            System.out.println( "usage: Server <portno>, defaulting to port " + portNo );
        } else {
            portNo = new Integer( args[0] );
        }

        /*create and start the server*/
        Server s = new Server( portNo );
        s.start();

        /*create the read from the scanner commands to issue to the server*/
        Scanner scanner = new Scanner( System.in );
        while ( true ) {
            /*read the command*/
            String cmd = scanner.nextLine();
            
            /*handle command*/
            if ( cmd.equals( "stop" ) ) {
                s.terminate();
                return;
            } else if ( cmd.equals( "status" ) ) {
                s.printStatus();
            } else {
                System.err.println( "Server: Invalid command." );
            }
        }
    }
}
